Skip to content

Keep each timeline message's action row inside its message - #2257

Merged
brsbl merged 3 commits into
mainfrom
bb/timeline-actions-row-width-thr_w8uypnhb9x
Aug 22, 2026
Merged

Keep each timeline message's action row inside its message#2257
brsbl merged 3 commits into
mainfrom
bb/timeline-actions-row-width-thr_w8uypnhb9x

Conversation

@brsbl

@brsbl brsbl commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What was wrong

The hover-revealed action row under each timeline message sat in normal flow with no width bound, so it rendered at its natural width regardless of the message it belongs to. A two-letter bubble of 51px carrying three actions of 76px overhung its message by 25px on desktop. On a touch phone the latest message's inline row overhung a 54px bubble by 46px. Because the row was a sibling of the bubble inside a w-fit column, a wide row could also widen the column itself. The row was misaligned even where it fitted, because its outer glyph sat 4px from the bubble's edge, inside the bubble's 12px corner radius, so it read as hanging off the message.

What changed

The change touches MessageActionBar.tsx and ConversationMessageContent.tsx.

The row now fits the message it belongs to. The most icons that fit under the message's width are shown inline, and the rest collapse progressively into a trailing "…" menu. When not even one fits, the row is the "…" alone. On touch, older messages keep their existing "…"-only footer, and the most recent message follows the same rules as desktop.

  • The row's slot is measured with a ResizeObserver, and computeMessageActionRowLayout keeps the actions that fit and moves the remainder into the menu. The row is absolutely positioned inside a full-width slot, so it can never contribute intrinsic width to a fit-content message column.
  • The user bubble and its row sit in a sub-column sized by the bubble, so the measured budget is the bubble's width.
  • The row aligns to the message text. It is inset by the bubble's padding and border minus the icon's hit-box slack, 13px on desktop and 11px on touch. Prose rows are pulled out by the slack alone. The outer glyph edge then lands exactly on the text edge on both sides.
  • The "…" sits 4px from the last inline action instead of the row's 8px gap, so it reads as the row's continuation, and the row stays revealed while its own menu is open.
  • On touch, tapping "…" reveals the actions in place when they fit the timeline column with a 16px margin, reaching into the empty gutter beside a narrow bubble. When the column is too tight, the anchored popover opens instead, because it scrolls and cannot clip.
  • Copying from the revealed touch row confirms on the trigger that replaces the row. The row collapses on the same click that copies, so CopyButton unmounts before its own check can appear, and the inline copy carries no toast.
  • Menus size to their widest label instead of a fixed w-48 or w-44, taking the desktop menu from 192px down to 150px, capped so a long plugin label wraps instead of running off a narrow viewport.

No contract, wire, or CLI surface changed.

Behavior breakpoints

Every pair below was captured in the real dev app from this checkout, using the same thread, route and window bounds, with the product code as the only variable. The before column comes from merge base c3b588c8f, and the after column comes from the PR head. Numbers are measured from the DOM, and "overhang" is how far the controls extend past their message.

Desktop 1280px, wide bubble

All three actions fit, so none collapse. This is unchanged apart from the alignment.

Before After

Desktop 1280px, short bubble of 51px

Before, three actions span 76px, a 25px overhang. After, nothing fits, so the row is the "…" alone.

Before After

Desktop, the menu holds what did not fit

Progressive overflow

Eight actions. In a wide column all eight are inline. In a 160px column four fit and the other four move into the menu. This one is a Ladle story, because the real thread has only three actions and so shows either all or none.

Compact width 600px with a mouse, resting and then drawer

The same 25px overhang appears before. After, the menu opens as the shared bottom drawer at this width.

Before After Menu open (after)

Touch phone 390px, resting

Older messages keep the "…"-only footer. The most recent message follows the desktop rules. Here the latest agent reply spans the column, so its three actions are inline, while the latest user bubble of 54px fits none and shows the "…". Before, that bubble carried 100px of icons, a 46px overhang.

Before After

Touch phone, tapping "…" reveals the actions in place

The 358px column fits all three, so they appear in the gutter beside the bubble instead of in a popover.

Touch phone, popover fallback

Eight actions in a 160px column cannot fit, so the popover opens instead, on-screen.

How you verified

  • pnpm exec turbo run test --filter=@bb/app -- MessageActionBar passes 28 tests, 11 of them new. They cover the layout math at its boundaries, meaning all inline, progressive collapse, and everything in the menu. They also cover desktop collapse into the "More actions" menu, touch collapse into the popover, reveal-in-place when the column has room, collapse-on-choose, the popover fallback when the column is too tight, and the copy confirmation on the trigger. The new assertions fail against the pre-change component.
  • pnpm exec turbo run test --filter=@bb/app -- "thread/timeline" passes 208 tests.
  • pnpm exec turbo run typecheck --filter=@bb/app is clean, and so are oxlint and oxfmt after the repository moved to Oxc in Migrate linting and formatting to Oxc #2258.
  • Every breakpoint above was driven by hand in the real dev app over the Chrome DevTools Protocol (CDP). A DOM audit asserts each row's control bounds against its message, giving 0px overhang everywhere. It also asserts the glyph against the message text edge, giving a 0.0px delta on both sides at both breakpoints.

One thing is not verified. An installed PWA was never exercised, because display-mode: standalone cannot be emulated over CDP. The touch path keys off (pointer: coarse) and (max-width: 767px), and the standalone rule in app.css only sets shell height, which this row does not read.

BB-Thread-ID: thr_w8uypnhb9x

AGENT GENERATED: by Claude Opus 5

@brsbl
brsbl force-pushed the bb/timeline-actions-row-width-thr_w8uypnhb9x branch 4 times, most recently from d124c9b to 0eec59b Compare August 22, 2026 03:43
@brsbl
brsbl marked this pull request as ready for review August 22, 2026 04:47
brsbl added 3 commits August 21, 2026 22:14
The hover-revealed action row under a timeline message sat in normal flow
with no width bound, so it rendered at its natural width regardless of the
message above it. A two-letter bubble (51px) carrying three actions (76px)
overhung by 25px on desktop; on a touch phone the latest message's inline
row overhung a 54px bubble by 46px.

Bound the row to the message it belongs to and collapse what does not fit:

- Measure the row's slot with a ResizeObserver and keep only the actions
  that fit; the rest move into a trailing "..." menu. The row is absolutely
  positioned inside a full-width slot so a wide row can never widen a
  fit-content message column.
- Wrap the user bubble and its row in a sub-column sized by the bubble, so
  the measured slot is exactly the bubble's width.
- On touch, tapping "..." expands the hidden actions in place when the whole
  set fits the timeline column with room to spare, reaching into the empty
  gutter beside a narrow bubble. When the column is too tight the anchored
  popover is used instead, since it scrolls and cannot clip.
- Size both menus to their widest label instead of a fixed width, and keep
  the row revealed while its own menu is open.
The row sat flush with the message's border box, so its outer glyph landed
4px from a bubble's edge — inside the bubble's 12px corner radius, reading as
if it hung off the message. On the agent side the same slack pushed the glyph
4px inside the prose edge, indented the other way.

Align the outer glyph edge to the message's text edge instead: inset the row
by the bubble's padding and border minus the icon's hit-box slack (13px
desktop, 11px touch), and pull prose rows out by the slack alone. The slot
carries the inset as padding so the measured budget is the text width the row
must fit, and the row carries a matching offset because an absolutely
positioned child resolves `right` against the padding box.
Tapping Copy in the revealed touch row collapsed the row on the same click,
unmounting CopyButton before its check could appear. The inline copy carries
no toast either (`useClipboardCopy` defaults `successMessage` to null), so on
every message but the most recent — where nothing is inline and the row is
just the trigger — copying gave no confirmation at all. The popover this
branch replaced kept its own check for two seconds.

Dispatch the revealed row's copy through `copyToClipboardWithToast` and
confirm on the trigger that replaces the row, matching the popover.
@brsbl
brsbl force-pushed the bb/timeline-actions-row-width-thr_w8uypnhb9x branch from 82ad0e5 to d3671a9 Compare August 22, 2026 05:15
@brsbl
brsbl merged commit f54b2f8 into main Aug 22, 2026
24 of 25 checks passed
@brsbl
brsbl deleted the bb/timeline-actions-row-width-thr_w8uypnhb9x branch August 22, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant